package proc

import (
	"context"
	"errors"
	"io"
	"log"
	"os/exec"
	"sync"
	"time"
)

var pMap sync.Map

func Watch() {
	for {
		pMap.Range(func(key, value interface{}) bool {
			p := value.(*Proc)
			if p.KeepAlive && p.Status != 1 {
				p.start()
				go p.readOut()
				go p.readErr()
				go p.wait()
			}
			return true
		})
		time.Sleep(time.Second * 6)
	}
}

type Proc struct {
	Key        string   `json:"key"`
	Path       string   `json:"path"`
	Args       []string `json:"args"`
	Dir        string   `json:"dir"`
	Background bool     `json:"background"`
	KeepAlive  bool     `json:"keepAlive"`
	Pid        int      `json:"pid"`
	Exited     bool     `json:"exited"`
	ExitCode   int      `json:"exitCode"`
	StartAt    int64    `json:"startAt"`
	ExitAt     int64    `json:"exitAt"`

	LastOutStr string `json:"lastOutStr"`
	LastErrStr string `json:"lastErrStr"`
	Status     int    `json:"status"`

	cmd     *exec.Cmd          `json:"-"`
	ctx     context.Context    `json:"-"`
	cancel  context.CancelFunc `json:"-"`
	stdout  io.ReadCloser      `json:"-"`
	stderr  io.ReadCloser      `json:"-"`
	outBuf  []byte             `json:"-"`
	errBuf  []byte             `json:"-"`
	outN    int                `json:"-"`
	errN    int                `json:"-"`
	lastErr error              `json:"-"`

	lock sync.RWMutex
}

func NewProc(key string, path string, args []string, dir string, background bool, keepAlive bool) (p *Proc) {
	if args == nil {
		args = []string{}
	}
	p = &Proc{
		Key:        key,
		Path:       path,
		Args:       args,
		Dir:        dir,
		Background: background,
		KeepAlive:  keepAlive,

		Status: 0,

		outBuf: make([]byte, 1024),
		errBuf: make([]byte, 1024),
	}
	return
}

func (p *Proc) start() (err error) {
	p.lock.Lock()
	defer p.lock.Unlock()
	if !p.Exited && p.cmd != nil {
		err = errors.New(p.Key + " running")
		p.lastErr = err
		return
	}
	var ctx0 context.Context
	if p.Background {
		ctx0 = context.Background()
	} else {
		ctx0 = context.TODO()
	}
	ctx, cancel := context.WithCancel(ctx0)
	p.cmd = exec.CommandContext(ctx, p.Path, p.Args...)
	p.ctx = ctx
	p.cancel = cancel
	if p.Dir != "" {
		p.cmd.Dir = p.Dir
	} else {
		p.Dir = p.cmd.Dir
	}

	stdout, err := p.cmd.StdoutPipe()
	if err != nil {
		p.lastErr = err
		return
	}
	p.stdout = stdout
	stderr, err := p.cmd.StderrPipe()
	if err != nil {
		p.lastErr = err
		return
	}
	p.stderr = stderr

	if err = p.cmd.Start(); err != nil {
		p.lastErr = err
		return
	}

	p.Pid = p.cmd.Process.Pid
	p.StartAt = time.Now().UnixNano() / 1e6
	p.ExitAt = 0
	p.Exited = false
	p.ExitCode = 0
	p.Status = 1
	return
}

func (p *Proc) wait() (err error) {
	err = p.cmd.Wait()
	if err != nil {
		p.lastErr = err
	}
	p.lock.Lock()
	defer p.lock.Unlock()
	p.Exited = p.cmd.ProcessState.Exited()
	p.ExitCode = p.cmd.ProcessState.ExitCode()
	p.ExitAt = time.Now().UnixNano() / 1e6
	p.Status = 2
	p.Pid = 0
	return
}

func (p *Proc) stop() {
	p.lock.Lock()
	defer p.lock.Unlock()
	if p.cancel != nil {
		p.cancel()
	}
}

func (p *Proc) readOut() {
	defer p.stdout.Close()
	for {
		n, err := p.stdout.Read(p.outBuf)
		if err != nil || io.EOF == err {
			log.Println(p.Key + " outReader err: " + err.Error())
			break
		}
		if n > 0 {
			p.outN = n
		}
	}
}

func (p *Proc) readErr() {
	defer p.stderr.Close()
	for {
		n, err := p.stderr.Read(p.errBuf)
		if err != nil || io.EOF == err {
			log.Println(p.Key + " errReader err: " + err.Error())
			break
		}
		if n > 0 {
			p.errN = n
		}
	}
}

func (p *Proc) out() string {
	p.LastOutStr = string(p.outBuf[0:p.outN])
	return p.LastOutStr
}

func (p *Proc) err() string {
	p.LastErrStr = string(p.errBuf[0:p.errN])
	return p.LastErrStr
}
